#include <LiquidCrystal.h>

#define ALIEN_MAX_HEALTH 3
#define PROJECTILE_HEALTH 1
#define INIT_PLAYER_HEALTH 3

#define INIT_SPAWN_PROBABILITY 30

#define PROJECTILE_CHAR '-'
#define ALIEN_CHAR 'D'
#define PLAYER_CHAR 'o'
#define EXPLOSION_CHAR 'x'

#define MATRIX_HEIGHT 16
#define MATRIX_WIDTH 2

typedef const unsigned int Pin;

Pin BUTTON_RIGHT = 6;
Pin BUTTON_LEFT = 7;
Pin BUTTON_RESET = 8;
Pin BUZZER = 9;

int shoot_interval = 666;
long int last_projectile_spawn = 0;

int alien_spawn_interval = 333;
long int last_alien_spawn = 0;
int spawn_probability = INIT_SPAWN_PROBABILITY;
int prob_increase_interval = 2000;
long int last_prob_increase = 0;

unsigned int player_pos = 0;
int player_health = 3;

int update_interval = 200;
long int last_update = 0;
bool gameover = false;

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

int alien_matrix[MATRIX_HEIGHT][MATRIX_WIDTH];
int projectile_matrix[MATRIX_HEIGHT][MATRIX_WIDTH];
int explosion_matrix[MATRIX_HEIGHT][MATRIX_WIDTH];

/*
* Print a variable's name and its value
*/
void debug(String varname, int value) {
  Serial.print(varname);
  Serial.print(":\t");
  Serial.println(value);
}

void input_manage() {

  int button_left = digitalRead(BUTTON_LEFT);
  int button_right = digitalRead(BUTTON_RIGHT);

  if(button_left == HIGH) {
    player_pos = 1;
  }

  if(button_right == HIGH) {
    player_pos = 0;
  }

  if(player_pos >= MATRIX_WIDTH)
    player_pos = MATRIX_WIDTH - 1;

  if(player_pos < 0)
    player_pos = 0;

  // Send button state to serial monitor
  debug("Left Button", button_left);
  debug("Right Button", button_right);
}

void physics_step_aliens() {

  for(int i = 0; i < (MATRIX_HEIGHT - 1); i++) {
    for(int l = 0; l < MATRIX_WIDTH; l++) {
      if(alien_matrix[i][l] > 0) {
        alien_matrix[i + 1][l] = alien_matrix[i][l];
        alien_matrix[i][l] = 0;
        i++;
      }
    }
  }

}

void physics_step_projectiles() {

  for(int i = 1; i < (MATRIX_HEIGHT - 1); i++) {
    for(int l = 0; l < MATRIX_WIDTH; l++) {
      if(projectile_matrix[i][l] > 0) {
        projectile_matrix[i - 1][l] = projectile_matrix[i][l];
        projectile_matrix[i][l] = 0;
      }
    }
  }
}

void physics_check_collisions() {


  for (int i = 0; i < MATRIX_WIDTH; ++i) {
    if(projectile_matrix[0][i] > 0)
      projectile_matrix[0][i] = 0;
  }

  for(int i = 0; i < MATRIX_HEIGHT; i++) {
    for(int l = 0; l < MATRIX_WIDTH; l++) {
      if(explosion_matrix[i][l] > 0)
        explosion_matrix[i][l]--;
    }
  }

  for(int i = 0; i < (MATRIX_HEIGHT - 1); i++) {
    for(int l = 0; l < MATRIX_WIDTH; l++) {
      if(projectile_matrix[i][l] > 0 && alien_matrix[i][l]) {
        projectile_matrix[i][l]--;
        alien_matrix[i][l]--;

        if(alien_matrix[i][l] == 0) {
          explosion_matrix[i][l] = 5;
          tone(BUZZER, 500, 20);
        }
      }
    }
  }

  for (int i = 0; i < MATRIX_WIDTH; ++i) {
    if(alien_matrix[(MATRIX_HEIGHT - 1)][i] > 0) {
      alien_matrix[(MATRIX_HEIGHT - 1)][i] = 0;
      player_health--;
      tone(BUZZER, 4500, 15);
    }
  }

}

void spawn_projectiles() {

  if(millis() - last_projectile_spawn > shoot_interval) {
    last_projectile_spawn = millis();
    projectile_matrix[MATRIX_HEIGHT - 2][player_pos] = PROJECTILE_HEALTH;
  }

}

void spawn_aliens() {

  if(millis() - last_prob_increase > prob_increase_interval) {
    last_prob_increase = millis();
    spawn_probability += 1;
  }

  if(millis() - last_alien_spawn < alien_spawn_interval)
    return;

  last_alien_spawn = millis();

  unsigned int prob = random(0, 100);
  unsigned int num = random(1, MATRIX_WIDTH);

  for(int i = 0; i < num; i++) {

    if(prob > (100 - spawn_probability)) {

      unsigned int pos = random(0, MATRIX_WIDTH);
      unsigned int health = 1;

      alien_matrix[0][pos] = health;
    }
  }
}

void render() {

  char framebuffer[MATRIX_HEIGHT][MATRIX_WIDTH];

  for(int i = 0; i < MATRIX_HEIGHT; i++) {
    for(int l = 0; l < MATRIX_WIDTH; l++) {
      framebuffer[i][l] = ' ';
    }
  }

  for(int i = 0; i < MATRIX_HEIGHT; i++) {
    for(int l = 0; l < MATRIX_WIDTH; l++) {
      if(projectile_matrix[i][l] > 0)
        framebuffer[i][l] = PROJECTILE_CHAR;
    }
  }

  for(int i = 0; i < MATRIX_HEIGHT; i++) {
    for(int l = 0; l < MATRIX_WIDTH; l++) {
      if(explosion_matrix[i][l] > 0)
        framebuffer[i][l] = EXPLOSION_CHAR;
    }
  }

  for(int i = 0; i < MATRIX_HEIGHT; i++) {
    for(int l = 0; l < MATRIX_WIDTH; l++) {
      if(alien_matrix[i][l] > 0)
        framebuffer[i][l] = ALIEN_CHAR;
    }
  }

  framebuffer[(MATRIX_HEIGHT - 1)][player_pos] = PLAYER_CHAR;

  lcd.setCursor(0, 0);
  for(int i = 0; i < MATRIX_HEIGHT; i++)
    lcd.print(framebuffer[i][0]);
  lcd.setCursor(0, 1);
  for(int i = 0; i < MATRIX_HEIGHT; i++)
    lcd.print(framebuffer[i][1]);
}

void welcome() {
  lcd.setCursor(0, 0);
  lcd.print("Welcome to");
  lcd.setCursor(0, 1);
  lcd.print("Space Invaders");
  delay(2500);
  lcd.clear();
}

void init_matrix() {
  
  // Initialize alien matrix
  for(int i = 0; i < MATRIX_HEIGHT; i++) {
     for(int l = 0; l < MATRIX_WIDTH; l++) {
       alien_matrix[i][l] = 0;
     }
  }

  // Initialize projectile matrix
  for(int i = 0; i < MATRIX_HEIGHT; i++) {
     for(int l = 0; l < MATRIX_WIDTH; l++) {
       projectile_matrix[i][l] = 0;
     }
  }

  for(int i = 0; i < MATRIX_HEIGHT; i++) {
     for(int l = 0; l < MATRIX_WIDTH; l++) {
       explosion_matrix[i][l] = 0;
     }
  }
}

void setup() {

  lcd.begin(MATRIX_HEIGHT, MATRIX_WIDTH);
  welcome();

  init_matrix();

  pinMode(BUTTON_RIGHT, INPUT);
  pinMode(BUTTON_LEFT, INPUT);
  pinMode(BUTTON_RESET, INPUT);

  Serial.begin(9600);
}

void reset() {
  gameover = false;
  player_health = INIT_PLAYER_HEALTH;
  player_pos = 0;
  spawn_probability = INIT_SPAWN_PROBABILITY;
  init_matrix();
  welcome();
}

void loop() {

  if(digitalRead(BUTTON_RESET) == HIGH) {
    reset();
  }

  if(gameover)
    return;

  if(player_health < 1) {
    gameover = true;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("GAMEOVER");
  }

  // Manage input through buttons
  input_manage();

  if(millis() - last_update> update_interval) {

    last_update = millis();

    // Make the aliens and the projectile advance one place
    physics_step_aliens();

    physics_check_collisions();

    physics_step_projectiles();

    // If the player can shoot, spawn a projectile
    spawn_projectiles();

    // Randomly spawn some aliens
    spawn_aliens();

    // Check for collisions between projectiles and aliens and between aliens and the player
    physics_check_collisions();

    // Render the scene by printing to the LCD screen
    render();

    debug("spawn_probability", spawn_probability);
  }
}

